madness

0.0.0-SNAPSHOT


Static site generator, based on Enlive and Bootstrap.

dependencies

org.clojure/clojure
1.4.0
enlive/enlive
1.0.1
clj-time
0.4.4
fs
1.3.2
clj-yaml
0.4.0
org.pegdown/pegdown
1.1.0
me.raynes/conch
0.4.0



(this space intentionally left almost blank)
 

Madness

Madness is a static site generator tool, primarily aimed at generating a blog, but also supports static pages too. It was primarily built to support my own needs when I rebuilt my own site, but perhaps it may prove useful for others aswell.

Source is available on github, and this documentation is also available online.

The source code, the design and the posts on the asylum branch are all under a CC-BY-SA-3.0 license.

Requirements

I only had a few simple requirements when I looked for a static site generator:

  • Simple updating: I do not want a complicated process, one that involves copying one directory over another. If I want to upgrade from one version of the generator tool to the other, all I want to do, is merge, and resolve conflicts. Perhaps update my templates, but that's about it.

    I have a version control system for a reason, layering over that is counter productive.

  • For similar reasons, I absolutely hate when templates are split into many many small files. I prefer them in one, as long as that makes sense.

    Since I do not need neither plugins, nor anything fancy like that, this works very well.

  • The templates must not contain any code, at all. They're plain HTML mocks, at most with ids or classes added so that the generator can easily recognise parts of the template. But no code ever, should be allowed in a template. This rules out pretty much every templating language out there.

    Thankfully, with Enlive, this goal could be easily achieved.

  • The engine must also support per-tag feeds, because if and when I decide to start publishing to a planet, it makes sense to only publish those parts of my blog, that are relevant for that particular planet.

Features

I found no static site generator that supported all of the above, and came with a reasonable theme aswell, so I ended up writing my own engine, and my own theme, and ended up with the following features:

  • Easy to update: I work on two branches, master and asylum. The former has no templates, no posts, no pages, just the code. The latter is all about the templates, posts, pages, and has no extra code.

    Therefore, merging between them is easy. If there'd be any outside contributions, either to the theme, or the code, those would be straightforward to handle too.

  • Single template for a single output format. I have no plugins to support, no need to include bits and pieces from other files. I can afford to have a single template, that I can preview in a browser as-is, and immediately do changes there.

  • The templates are pure data, there is no code in them. Every bit of logic is within the code itself, the templates are pure.

  • The engine supports per-tag archives and per-tag feeds, implementing date-based archives or feeds wouldn't be hard, either.

  • It is written in a sane language, one that I feel comfortable with, one I can understand months later, even if I didn't write neither tests nor documentation originally (I really should have done both, though, out of principle if nothing else).

The templates

While the main master branch does not have any default template, the asylum branch does: one built upon Twitter's Bootstrap, in such a way that it displays well on any display ranging from smartphones to huge desktops.

At least, that was the plan, and that's how it works on everything I tested it with so far.

It's responsive, straightforward, and quite simple too, as far as I can tell. It also degrades well even down to text-mode browsers.

(ns madness)
 

Entry point into madness, usage:

$ lein run -m madness.core
$ lein run -m madness.core :index :archive
(ns madness.core
  ^{:author "Gergely Nagy <algernon@madhouse-project.org>"
    :copyright "Copyright (C) 2012 Gergely Nagy <algernon@madhouse-project.org>"
    :license {:name "Creative Commons Attribution-ShareAlike 3.0"
              :url "http://creativecommons.org/licenses/by-sa/3.0/"}}
  (:require [madness.render :as render]))

Takes a string, strips the first char (assumed to be :), and returns a keyword.

(defn- str->keyword
  [s]
  (keyword (apply str (rest s))))

The main entry point of Madness: when called without arguments, generates everything. If called with a list of (string) keywords, only generates the given parts of the blog.

The following keywords are understood:

  • :index: The main index page of the blog.
  • :archive: The main archive page of the blog.
  • :tags: All of the per-tag archives of the blog.
  • :date-archives: Yearly, monthly & daily archives of the blog.
  • :posts: All of the posts that belong to the blog.
  • :pages: All of the pages that accompany the blog.
  • :main-feed: The main Atom feed.
  • :tag-feeds: The per-tag Atom feeds.
  • :date-feeds: Yearly, monthly & daily atom feeds.
(defn -main
  ([] (-main ":index" ":archive" ":tags" ":date-archives" ":posts"
             ":main-feed" ":pages" ":tag-feeds" ":date-feeds"))
  ([& args] (dorun (map render/render (map str->keyword args)))))

Render post fragments, posts that are not wrapped within the overall design.

Takes one ore more post URLs, and renders them to the standard output.

(defn madness-fragments
  [& post-urls]
  (dorun (map (partial render/render :post-fragment) post-urls)))
 

Configuration handling for Madness.

Madness can be configured by placing a file called settings.clj in the root directory. Settings therein will override the defaults when the file gets evaluated.

(ns madness.config
  ^{:author "Gergely Nagy <algernon@madhouse-project.org>"
    :copyright "Copyright (C) 2012-2013 Gergely Nagy <algernon@madhouse-project.org>"
    :license {:name "Creative Commons Attribution-ShareAlike 3.0"
              :url "http://creativecommons.org/licenses/by-sa/3.0/"}}
  ; Dummy :use, so that the meta-data above gets parsed correctly.
  (:use [clojure.core]))

The default configuration values, settings in settings.clj override these values.

The settings are as follows:

  • :template is a map of filenames to use as templates for various roles, such as: :default, :atom, and :empty.

    The first is a one-file template to use for the whole site. It should include all the bits and pieces needed to build the site, more about that in the next section.

    The second is the template for Atom feeds: the main feed and the per-tag feeds alike.

    The last one should be an almost empty HTML file, containing only an empty body element.

  • :dirs, the directories where the various parts of the sources are to be found.

  • :recent-posts and :archived-posts both have a :columns and a :rows setting, which determines how many columns and rows each will have. Setting :rows to 0 disables limiting.

  • :atom controls the :base-url and the :title of the generated Atom feeds.

(def default-config
  {:template {:default "default.html"
              :atom "atom.xml"
              :empty "empty.html"}
   :dirs {:posts "resources/posts"
          :pages "resources/pages"
          :output "public"}
   :recent-posts {:columns 3
                  :rows 2}
   :archive-posts {:columns 3
                   :rows 0}
   :atom {:base-url "http://localhost"
          :title nil}})

The final configuration for Madness - settings.clj merged into the default-config.

(def config
  (merge-with merge default-config (eval (read-string (slurp "settings.clj")))))

Get the location of a template. Without arguments, returns the location of the default template, otherwise the specified one.

(defn template
  [& id]
  (str "templates/" ((or (first id) :default) (:template config))))

Return various settings for recent-posts. Apart from the settable :rows and :columns setting, this understands :total and :span too, where the first returns the total number of recent posts to display, and the latter the amount of Bootstrap grid columns a single item should span.

(defmulti recent-posts
  identity)

By default, whatever setting was asked for, we look that up in config's :recent-posts map.

(defmethod recent-posts :default [setting]
  (-> config :recent-posts setting))

However, if we want the :total number of recent posts, we either look that up from the config, or simply multiply the number of rows and columns, and add one, so that the result is suitable for range.

(defmethod recent-posts :total [_]
  (or (-> config :recent-posts :total inc)
      (inc (* (recent-posts :columns)
              (recent-posts :rows)))))

And to determine how many columns a single recent item should span, we divide 12 by the number of columns, and round it.

(defmethod recent-posts :span [_]
  (int (/ 12 (recent-posts :columns))))

Return various settings for archive-posts. Apart from the settable :rows and :columns setting, this understands :span too, which is the amount of Bootstrap grid columns a single archived item should span

(defmulti archive-posts
  identity)

By default, whatever setting was asked for, we look that up in config's :archive-posts map.

(defmethod archive-posts :default [setting]
  (-> config :archive-posts setting))

And to determine how many columns a single archive item should span, we divide 12 by the number of columns, and round it.

(defmethod archive-posts :span [_]
  (int (/ 12 (archive-posts :columns))))

Looks up a directory in config's :dirs map.

(defn dirs
  [role]
  (-> config :dirs role))

Looks up a setting in config's :atom map.

(defn atom-feed
  [setting]
  (-> config :atom setting))
 

Madness resources

Pretty much for convenience only, and to not reload all the posts and pages every time we need them, this namespace contains a few convenience Vars, with pages and posts preloaded.

These are heavily used by madness.render.

(ns madness.resources
  ^{:author "Gergely Nagy <algernon@madhouse-project.org>"
    :copyright "Copyright (C) 2012 Gergely Nagy <algernon@madhouse-project.org>"
    :license {:name "Creative Commons Attribution-ShareAlike 3.0"
              :url "http://creativecommons.org/licenses/by-sa/3.0/"}}
  (:require [madness.blog :as blog]
            [madness.utils :as utils]))

Convenience Var containing all the blog posts

(defonce posts (blog/load-posts))

Convenience Var contiaining all the blog pages

(defonce pages (blog/load-pages))

Convenience Var containing all the blog posts, grouped by tags.

(defonce posts-tag-grouped (utils/group-blog-by-tags posts))
 

High-level rendering

In this part of the documentation, the various parts of the templates will be explained in detail: how they are built up, how madness recognises various parts of it, and so on and so forth. This is a high-level overview, but pointers will be given to places that go into the tinies details.

For a full-blown example, check the asylum branch of this project, which has templates, posts and pages too.

(ns madness.render
  ^{:author "Gergely Nagy <algernon@madhouse-project.org>"
    :copyright "Copyright (C) 2012-2013 Gergely Nagy <algernon@madhouse-project.org>"
    :license {:name "Creative Commons Attribution-ShareAlike 3.0"
              :url "http://creativecommons.org/licenses/by-sa/3.0/"}}
  (:require [madness.io :as io]
            [madness.blog :as blog]
            [madness.blog.index :as blog-index]
            [madness.blog.archive :as blog-archive]
            [madness.blog.post :as blog-post]
            [madness.blog.page :as blog-page]
            [madness.blog.atom :as blog-feed]
            [madness.config :as cfg]
            [madness.utils :as utils]
            [madness.resources :as res]))

Render a post or page to a file, using a custom render function.

(defn- render-to-file
  [all-posts current-post render-fn file]
  (io/write-out-dir (utils/replace-extension file ".html")
                    (apply str (render-fn current-post all-posts))))

Render a post or page to stdout, using a custom render function.

(defn- render-to-stdout
  [all-posts current-post render-fn]
  (println (apply str (render-fn current-post all-posts))))

Render a part of the site to files.

Rendering

Every part of the site - at least the HTML parts - have a common structure: a navigation bar, the content area, and a footer, and whatever else the template may contain. These are common to all pages and posts, archives and everything else too.

What is inside the content area, varies by what page we're talking about - as it will be explained just below.

(defmulti render
  (fn [part & args] part))

The site index

Renders the main entry point of the site into the content area, and saves the result into a file named index.html.

The content area will consist of the latest blog post, followed by a limited number of recent items: the configuration controls how many of these are displayed at most.

Blog entries that are too old to fit, will only be available through the archive, and they're not displayed on the index at all.

For more information about how the content area is built, see the blog.index namespace.

(defmethod render :index [_]
  (render-to-file nil res/posts blog-index/blog-index "index.html"))

The global archive

Renders the global archive to a file named blog/archives/index.html.

The archive is not much more than a title in place of a highlighted post, followed by a limited set of recent posts, then the archived posts.

The archived posts are rendered differently than the recent posts (see the blog.archive namespace!), and by default, their number is not limited. Madness does not implement any kind of archive paging.

(defmethod render :archive [_]
  (render-to-file res/posts res/posts
                  (partial blog-archive/blog-archive "Archive" "/blog/atom.xml")
                  "blog/archives/index.html"))

The tag archives

Each tag has an archive of its own, in files named after the tag, something like blog/tag/index.html.

These pages are exactly the same as the global archive above, except that in the recent and archived posts area, only those posts are shown, that are tagged with the appropriate tag.

To render all the archives for each and every tag, we need a function that can render one.

(defmethod render :tag-archive
  [_ all-posts tag tagged-posts]
  
  (let [fn (str "." (utils/tag-to-url tag) "index.html")]
    (render-to-file all-posts tagged-posts
                    (partial blog-archive/blog-archive (str "Tag: " tag)
                             (str "" (utils/tag-to-url tag) "atom.xml")) fn)))

And another, that maps through the tags, and using the previous method, renders an archive for each of them.

(defmethod render :tags [_]
  (dorun (map #(render :tag-archive res/posts %1
                       (get res/posts-tag-grouped %1))
              (keys res/posts-tag-grouped))))

Date-based archives

Because posts are rendered into locations based on their creation date, and because it makes it easier to navigate bigger blogs, we need archives for each year, month and date.

These dated archives are exactly the same as the global archive, except that the recent and archived posts are limited to a particular date: a year, a month, or a day.

To render all the archives for each and every date a post was created on, we need a function that can render one.

(defmethod render :date-archive
  [_ all-posts date dated-posts]

  (let [uri (str "/blog/" date "/")
        fn (str "." uri "index.html")]
    (render-to-file all-posts dated-posts
                    (partial blog-archive/blog-archive
                             (str "Archive of posts @ " date)
                             (str "" uri "atom.xml")) fn)))

And since all the dated archives follow the same pattern, lets introduce a helper function!

Group blog posts by date, using the function f (which is expected to return a formatted date when given a blog post), and render the archives for each and every key within the group.

(defn render-dated-archive
  [render-type f]
  (let [dated-archive (utils/group-blog-by-date res/posts f)]
    (dorun (map #(render render-type res/posts %1 (get dated-archive %1))
                (keys dated-archive)))))

With these, we can render daily, montly and yearly archives easily.

(defmethod render :daily-archives [_]
  (render-dated-archive :date-archive utils/posts-by-day))
(defmethod render :monthly-archives [_]
  (render-dated-archive :date-archive utils/posts-by-month)
  (render :daily-archives))
(defmethod render :yearly-archives [_]
  (render-dated-archive :date-archive utils/posts-by-year)
  (render :monthly-archives))

And for convenience, we have a function that can be called from the command line, and will generate all of the above dated archives.

(defmethod render :date-archives [_]
  (render :yearly-archives))

Blog posts

Blog posts are pretty simple: they show the post itself, the date it was posted, the tags it is tagged with, and any previous or next articles, for easier navigation.

There is no recent post or archived posts area here, but commenting can be enabled on a per-post basis.

For more information on how a post is rendered, see the blog.post namespace.

Similar to how it was done with the tag archives before, to render all posts, we must first be able to render one. Fortunately for us, blog posts have an :url property, which we can re-use for their filename.

By default (see [blog.post1), this will be of the YEAR/MONTH/DATE/title/ format, to which, we append index.html.

(defmethod render :post
  [_ all-posts post]

  (let [fn (str "." (:url post) "index.html")]
    (render-to-file all-posts post blog-post/blog-post fn)))

And now that we can render a single post, we shall render them all!

(defmethod render :posts [_]
  (dorun (map (partial render :post res/posts) res/posts)))

To help cross-posting to other engines, it is useful to be able to render only the content of a post, a fragment, without the rest of the page wrapped around it.

These fragments shall be rendered to standard output, as they should not hit the disk by default.

(defmethod render :post-fragment
  [_ post-url]

  (let [post (first (filter #(= (:url %) post-url) res/posts))]
    (dorun (render-to-stdout nil post blog-post/blog-post-fragment))))

Static pages

Static pages are very much like blog posts, except they are not tagged, there is no previous or next page, and their URL is not date based, but determined (see blog.page) by their place on the filesystem.

As always, we render a single page first.

(defmethod render :page
  [_ all-posts page]

  (let [fn (str "." (:url page))]
    (render-to-file all-posts page blog-page/blog-page fn)))

Then map through all of them, to render them all.

(defmethod render :pages [_]
  (dorun (map (partial render :page res/posts) res/pages)))

Atom feeds

Atom feeds are a bit different than the HTML representation, the feed has a well defined spec. We're going to generate two kinds of feeds: a global one, including all blog posts (but only blog posts, not pages), and per-tag feeds, that only include posts for the particular tag.

For more information about how a feed is assembled, see the blog.atom namespace!

The main feed will be saved into blog/atom.xml.

(defmethod render :main-feed [_]
  (io/write-out-dir "blog/atom.xml"
                    (blog-feed/emit-atom (cfg/atom-feed :title) "/blog/"
                                         res/posts)))

A single per-tag feed is saved into something like blog/tag/atom.xml, similarly to how the per-tag archives were rendered.

(defmethod render :tag-feed
  [_ tag tagged-posts]

  (let [fn (str "." (utils/tag-to-url tag) "atom.xml")]
    (io/write-out-dir fn
                      (blog-feed/emit-atom
                       (str (cfg/atom-feed :title) ": " tag)
                       (utils/tag-to-url tag)
                       tagged-posts))))

And as usual, since we can render a feed for a single tag, mapping through all the tags is all it takes to render them all.

(defmethod render :tag-feeds [_]
  (dorun (map #(render :tag-feed %1 (get res/posts-tag-grouped %1))
              (keys res/posts-tag-grouped))))

As with archives, we render atom feeds for each year, month and day a blog post was posted on, in a very similar manner the archives are rendered.

(defmethod render :date-feed
  [_ _ date dated-posts]

  (let [uri (str "/blog/" date "/")
        fn (str "." uri "atom.xml")]
    (io/write-out-dir fn
                      (blog-feed/emit-atom
                       (str (cfg/atom-feed :title) " @ " date)
                       uri
                       dated-posts))))
(defmethod render :daily-feeds [_]
  (render-dated-archive :date-feed utils/posts-by-day))
(defmethod render :monthly-feeds [_]
  (render-dated-archive :date-feed utils/posts-by-month)
  (render :daily-feeds))
(defmethod render :yearly-feeds [_]
  (render-dated-archive :date-feed utils/posts-by-year)
  (render :monthly-feeds))
(defmethod render :date-feeds [_]
  (render :yearly-feeds))
 

I/O helper routines

(ns madness.io
  ^{:author "Gergely Nagy <algernon@madhouse-project.org>"
    :copyright "Copyright (C) 2012 Gergely Nagy <algernon@madhouse-project.org>"
    :license {:name "Creative Commons Attribution-ShareAlike 3.0"
              :url "http://creativecommons.org/licenses/by-sa/3.0/"}}
  (:require [madness.config :as cfg]
            [clojure.string :as str]
            [net.cgrand.enlive-html :as h]
            [fs.core :as fs]
            [clj-yaml.core :as y])
  (:import (java.io StringReader)
           (org.pegdown PegDownProcessor)))

List all HTML and Markdown files within a given directory. Returns an array of java.io.File objects.

(defn find-files
  [dir]
  (sort #(compare %2 %1) (fs/find-files dir #".*\.(html|md|markdown|mdwn)$")))

Given a filename, write a string into it, creating the file and the directories if needed. The destination directory can be overridden via the configuration mechanism.

(defn write-out-dir
  [file str]
  (let [fn (str/join "/" [(cfg/dirs :output) file])]
    (println "Writing" fn "...")
    (fs/mkdirs (fs/parent fn))
    (spit fn str :encoding "UTF-8")))

Given a file type (a file-name extension) and a file, transform it into a format that can be fed to Enlive, and conforms to the requirements set by the rendering engine.

(defmulti preprocess-file
  (fn [type file] type))

Split metadata (things between "---" lines at the top of the file) out of the file contents.

Returns a vector containing the metadata and the rest of the content.

(defn- split-metadata-and-content
  [content]
  (let [idx (.indexOf content "---" 4)]
    [(.substring content 4 idx) (.substring content (+ 3 idx))]))

Split the short summary and the body of a post into two. The end of the short summary must be marked with .

Returns a vector containing the short summary and the main contents.

(defn- split-summary-from-content
  [content]
  (let [idx (.indexOf content "<!-- more -->")]
    (if (= idx -1)
      ["" content]
      [(.substring content 0 idx) (.substring content (+ 13 idx))])))

Wrap a value within a HTML element.

(defn- html-wrap
  [e v]
  (str "<" e ">" v "</" e ">"))

Convert a key-value pairs data part into a HTML tag string. If the key is :tags, then the value shall be a list of elements. Otherwise the value is left as-is.

(defn- v->html
  [k, v]
  (if (= k :tags)
    (reduce (fn [h v] (str h (html-wrap "tag" v))) "" v)
    v))

Convert YAML-format meta-data to HTML string.

(defn- yaml->html-string
  [metadata]
  (let [meta (y/parse-string metadata)]
    (reduce (fn [h [k v]]
              (str h (html-wrap (name k) (v->html k v))))
            "" meta)))

Processing markdown files has three steps:

  • Split the metadata from the content, and parse the former as if it was YAML.
  • Split the short summary from the main data
  • Assemble these three into a HTML format that the processing engine expects.
(defmethod preprocess-file ".md" [_ file]
  (let [[metadata content] (split-metadata-and-content (slurp file))
        [summary content] (split-summary-from-content content)]
    (StringReader. (str (html-wrap "article" (yaml->html-string metadata))
                        (html-wrap "summary" (.markdownToHtml (PegDownProcessor.) summary))
                        (html-wrap "section" (.markdownToHtml (PegDownProcessor.) content))))))
(defmethod preprocess-file ".mdwn" [_ file]
  (preprocess-file ".md" file))
(defmethod preprocess-file ".markdown" [_ file]
  (preprocess-file ".md" file))

Anything that is not markdown, is let through as-is, and we'll assume it is HTML.

(defmethod preprocess-file :default [_ file]
  file)

Load and process a file. Based on the extension, the file will either be processed as raw HTML, or preprocessed as Markdown first.

(defn read-file
  [file]
  (let [content (preprocess-file (-> file .getName fs/extension)
                                 file)]
    (h/html-resource content)))
 

Assorted utilities.

(ns madness.utils
  ^{:author "Gergely Nagy <algernon@madhouse-project.org>"
    :copyright "Copyright (C) 2012 Gergely Nagy <algernon@madhouse-project.org>"
    :license {:name "Creative Commons Attribution-ShareAlike 3.0"
              :url "http://creativecommons.org/licenses/by-sa/3.0/"}}
  (:require [clojure.string :as s]
            [clj-time.format :as time-format]
            [net.cgrand.enlive-html :as h]
            [fs.core :as fs]
            [conch.sh :refer [let-programs]]))

A <hr> element that is only visible on desktop resolutions.

(def hr-desktop [{:tag :hr :attrs {:class "visible-desktop"}}])

Return the list of unique tags, sorted alphabetically.

(defn tags
  [blog]
  (apply sorted-set (mapcat :tags blog)))

Given a tag, return a relative URL that points to it.

(defn tag-to-url
  [tag]
  (str "/blog/tags/" (s/replace (s/lower-case tag) " " "-") "/"))

Format a date object into human-readable form.

(defn date-format
  [date]
  (time-format/unparse (time-format/formatter "yyyy-MM-dd") date))

Given a date, return a relative URL that points to the yearly archives.

(defn date-to-url
  [date]
  (str "/blog/" (time-format/unparse (time-format/formatter "yyyy") date) "/"))

Determine whether a post is tagged with a given tag.

(defn post-tagged?
  [post tag]
  (some #(= tag %1) (:tags post)))

Return a list of posts tagged with a given tag.

(defn posts-tagged
  [posts tag]
  (filter #(post-tagged? %1 tag) posts))

Group all blog posts by their tags. Posts may appear under multiple tags. Returns a hash-map, with the tags as keys, and the list of posts as values.

(defn group-blog-by-tags
  [blog]
  (reduce #(assoc %1 %2 (posts-tagged blog %2)) {} (tags blog)))

Returns a set of all the blog posts that match the filter.

(defn- blog-dates
  [blog f]
  (set (map #(f (:date %1)) blog)))

Checks whether a given post was made on a particular date. The f function is used to transform the posts date before the comparsion.

(defn- post-with-date?
  [date f post]
  (let [fdate (f (:date post))]
    (= date fdate)))

Returns all posts within a blog that have a particular date, where the date of the posts is determined after transforming them with function f.

(defn- posts-with-date
  [blog date f]
  (filter (partial post-with-date? date f) blog))

Group all posts within a blog by date. Uses the supplied f function to format and compare dates.

(defn group-blog-by-date
  [blog f]
  (reduce #(assoc %1 %2 (posts-with-date blog %2 f)) {}
          (blog-dates blog f)))
(defn posts-by-day
  [d]
  (time-format/unparse (time-format/formatter "yyyy/MM/dd") d))
(defn posts-by-month
  [d]
  (time-format/unparse (time-format/formatter "yyyy/MM") d))
(defn posts-by-year
  [d]
  (time-format/unparse (time-format/formatter "yyyy") d))

Given a full blog, and a single post, find the previous and the next post that surround the current one.

(defn neighbours
  [blog post]
  (if (= (first blog) post)
    [nil (second blog)]
    (loop [prev (first blog)
           posts (rest blog)]
      (if (= (first posts) post)
        [prev (second posts)]
        (recur (first posts) (rest posts))))))

Given a number of columns, rows and a list of blog-posts, arrange them into a table with the given number of rows and columns. If rows is set to zero, there will be no limit on how many rows the resulting table may have.

Returns an array where each element is a list of posts for that row.

(defn blog->table
  [columns rows blog-posts]
  (if (zero? rows)
    []
    (loop [posts blog-posts
           c 0
           result []]
      (if (or (empty? posts) (and (> rows 0) (>= c rows)))
        result
        (recur (drop columns posts)
               (inc c)
               (conj result (take columns posts)))))))

Rewrite an anchor element's href and content.

(defn rewrite-link
  [url content]
  (h/do->
   (h/set-attr :href url)
   (h/content content)))

Rewrite an anchor element's href, content and title. The content is the same as the title, with a single space prepended.

(defn rewrite-link-with-title
  [url title]
  (h/do->
   (h/set-attr :href url)
   (h/set-attr :title title)
   (h/content " " title)))

Replace the extension of a file with another.

(defn replace-extension
  [fn ext]
  (let [components (fs/split fn)
        fn (str (fs/name (last components)) ext)]
    (if (= "/" (first components))
      (s/join "/" (conj (vec (butlast (rest components))) fn))
      (s/join "/" (conj (vec (butlast components)) fn)))))

A very dumb little helper function, that merely checks if a value is set or not - it's mostly here to make some of the code below clearer.

(defn enabled?
  [value]
  (if (or
       (nil? value)
       (= value ""))
    false
    true))

Syntax highlight some code.

(defn pygmentize
  [language text]
  (let-programs [pygmentize "/usr/bin/pygmentize"]
                (pygmentize "-fhtml" (str "-l" language)
                            (str "-Ostripnl=False,encoding=utf-8")
                            {:in text})))

Syntax highlight a node. The node must have the language in the data-language attribute.

(defn pygmentize-node
  [node]
  (let [language (-> node :attrs :data-language)
        new-content (pygmentize language (:content node))]
    ((h/html-content new-content) node)))
 

Load blog posts & pages

To build a whole site, madness will first load all the posts and pages, and turn them into data structures that are easy to work with. This is far from efficient, but even for a moderately sized site, neither speed nor memory requirements are particularly bad.

(ns madness.blog
  ^{:author "Gergely Nagy <algernon@madhouse-project.org>"
    :copyright "Copyright (C) 2012, 2013 Gergely Nagy <algernon@madhouse-project.org>"
    :license {:name "Creative Commons Attribution-ShareAlike 3.0"
              :url "http://creativecommons.org/licenses/by-sa/3.0/"}}
  (:require [madness.blog.post :as blog-post]
            [madness.blog.page :as blog-page]
            [madness.config :as cfg]
            [madness.io :as io]))

Load all posts for the blog. Returns a sequence of processed blog posts, sorted by date, newest first. See the blog.post namespace for more information about how a processed post looks like.

(defn load-posts
  []
  (sort-by :date #(compare %2 %1)
           (map blog-post/read-post (io/find-files (cfg/dirs :posts)))))

Load all pages for the blog. Returns a sequence of processed blog pages. See the blog.page namespace for more information about how a processed page looks like.

(defn load-pages
  []
  (map blog-page/read-page (io/find-files (cfg/dirs :pages))))
 

Building blocks for navigation

All HTML pages of the generated site will contain some common navigation items: a global list of recent items, and the list of all available tags.

The functions herein implement the recent item & tag lists handling.

(ns madness.blog.nav
  ^{:author "Gergely Nagy <algernon@madhouse-project.org>"
    :copyright "Copyright (C) 2012-2013 Gergely Nagy <algernon@madhouse-project.org>"
    :license {:name "Creative Commons Attribution-ShareAlike 3.0"
              :url "http://creativecommons.org/licenses/by-sa/3.0/"}}
  (:require [net.cgrand.enlive-html :as h]
            [clojure.string :as s]
            [madness.config :as cfg]
            [madness.utils :as utils]))

The template must have a part with the id of #madness-recent-posts, under which must be an unordered list (with only one item in the template). The single item of that list will be used as the template snippet for displaying recent items.

The list item itself, must contain an anchor, whose href and content this snippet will change to the URL and title of the post, respectively.

(h/defsnippet recent-item (cfg/template) [:#madness-recent-posts :li]
  [title url]
  [:a] (utils/rewrite-link url title))

Similarly to recent items, the template must also contain a #madness-tags item, also with an unordered list beneath it, where the single element of the template must also have an anchor.

The anchor's href and textual content will be rewritten by this snippet to the URL for the tag archive, and the name of the tag itself.

(h/defsnippet tag-item (cfg/template) [:#madness-tags :li]
  [tag]
  [:a] (utils/rewrite-link (utils/tag-to-url tag) tag))

To render all recent posts, we simply clone the snippet above for every recent post we should display. How many are displayed, is controlled by the configuration.

(defn recent-posts
  [all-posts]
  (h/clone-for [post (take (dec (cfg/recent-posts :total)) all-posts)]
               (h/substitute (recent-item (:title post) (:url post)))))

And to display all tags, we also clone the tag-item snippet above for each and every unique tag.

(defn all-tags
  [all-posts]
  (h/clone-for [tag (sort #(compare (s/lower-case %1)
                                    (s/lower-case %2))
                          (utils/tags all-posts))]
               (h/substitute (tag-item tag))))

These last two functions - recent-posts and all-tags - will be used by the various page rendering templates in blog.archive, blog.post, and blog.page.

 

Recent posts on archive & index pages

The archive pages - along with the site index - will display a number of recent and (in case of archive pages) archived posts. The snippets herein lay the foundation for rendering those on all these pages.

One important thing to note, is that recent and archived posts are not displayed on non-desktop resolutions, unless we're rendering an archive page. That is, on a smartphone, the index page will not have any recent posts shown.

(ns madness.blog.recent
  ^{:author "Gergely Nagy <algernon@madhouse-project.org>"
    :copyright "Copyright (C) 2012-2013 Gergely Nagy <algernon@madhouse-project.org>"
    :license {:name "Creative Commons Attribution-ShareAlike 3.0"
              :url "http://creativecommons.org/licenses/by-sa/3.0/"}}
  (:require [net.cgrand.enlive-html :as h]
            [madness.utils :as utils]
            [madness.blog.post :as blog-post]
            [madness.config :as cfg]))

The first nippet to care about is the tag list of a recent post item. We use the element with a tag class under the #recent-posts element of the template as a basis for the snippet.

We replace the href (as it should be an anchor), and pick out a &lt;span> element from under it, and replace that with the tag's name. The reason we use a separate element for the tag name, is because we want to allow the template to also add an icon somewhere onto the tag button.

Madness does not enforce the icon, but makes it possible to use one.

This snippet is for a single tag, it will need to be cloned for all tags, see later!

(h/defsnippet recent-post-tag (cfg/template) [:#recent-posts :.tag]
  [tag]
  [:a] (h/do->
        (h/remove-class "tag")
        (h/set-attr :href (utils/tag-to-url tag))
        (h/after " "))
  [:a :span] (h/substitute tag))

We'll use the element with a recent-post class as the basis for a single recent post item. This should have a header (<h2>), and an link under it - the anchor's href and textual content will be rewritten appropriately.

The same recent-post element must also have a child with a post-date class, whose content will be substituted by the date the blog post was made upon.

Then there is the paragraph, with a class of summary, which will be replaced by the blog post's summary, and of course the tag classed element will be replaced by the list of tags (recent-post-tag above cloned for all tags).

(h/defsnippet blog-recent-meta (cfg/template)
  [:#madness-recent-article-meta]
  [post]
  [:#madness-recent-article-date] (h/do->
                                   (h/set-attr :href (utils/date-to-url (:date post)))
                                   (h/content (utils/date-format (:date post))))
  [:#madness-recent-article-tags :a] (h/clone-for
                               [tag (butlast (:tags post))]
                               (h/do->
                                (h/substitute (blog-post/blog-post-tag tag))
                                (h/after ", ")))
  [:#madness-recent-article-tags] (h/append
                            (blog-post/blog-post-tag (last (:tags post))))
  [:#madness-recent-article-tags] (h/remove-attr :id)
  [:#madness-recent-article-meta] (h/remove-attr :id))
(h/defsnippet recent-post-item (cfg/template) [:#madness-archive-recent-post]
  [post]
  [:#madness-archive-recent-post] (h/remove-attr :id)
  [:h3 :a] (utils/rewrite-link-with-title
             (:url post)
             (:title post))
  [:#madness-recent-article-meta] (h/substitute (blog-recent-meta post))
  [:#madness-recent-article-summary] (h/substitute (:summary post)))

And finally, we are now able to assemble a whole row of recent posts! We'll use the #recents element as a basis for our template.

Since there can be multiple rows, we'll remove the id, and for each item on the row, we'll clone the recent-post-item snippet, replacing the .recent-post element in the template.

If we're rendering an archive, we'll also remove the visible-desktop class, since archives should be visible on non-desktop resolutions too.

(h/defsnippet recent-post-row (cfg/template) [:#madness-archive-recent-post-row]
  [posts archive?]
  [:#madness-archive-recent-post-row] (h/remove-attr :id)
  [:#madness-archive-recent-post]
    (h/clone-for [p posts]
                 (h/do->
                  (h/substitute (recent-post-item p))
                  (h/set-attr :class (str "span" (cfg/recent-posts :span)))
                  (h/remove-attr :id))))
 

Loading & rendering individual blog posts

For the sake of ease, blog posts are fully loaded first, and turned into a structure that is easy to work with. This namespace implements the low-level loading, restructuring and rendering of individual blog posts.

Blog posts are under resources/posts by default, and they all must have a filename that starts with a date (in YYYY-MM-DD format), followed by the short title of the post, that will be used for the URL.

(ns madness.blog.post
  ^{:author "Gergely Nagy <algernon@madhouse-project.org>"
    :copyright "Copyright (C) 2012-2013 Gergely Nagy <algernon@madhouse-project.org>"
    :license {:name "Creative Commons Attribution-ShareAlike 3.0"
              :url "http://creativecommons.org/licenses/by-sa/3.0/"}}
  (:require [net.cgrand.enlive-html :as h]
            [madness.blog.nav :as blog-nav]
            [madness.utils :as utils]
            [madness.config :as cfg]
            [clojure.string :as str]
            [madness.io :as io]
            [clj-time.format :as time-format]))

The date format used within blog files. Note that this is not the format we render dates as, but the format we expect them to be within blog posts!

(def blog-date-format ^{:private true}
  (time-format/formatter "yyyy-MM-dd HH:mm"))

Given a parsed date and a source filename, return the URI for the blog post.

The source filenames will be stripped of the date part, and the date within the post itself will be used to construct the final URL of the form /blog/yyyy/MM/dd/title/.

(defn- post-url
  [date fn]
  (str "/blog/" (time-format/unparse (time-format/formatter "yyyy/MM/dd") date)
       "/" (second (first (re-seq #"....-..-..-(.*)\.([^\.]*)$" fn))) "/"))

Read a blog post from a file, and restructure it into a representation that is easy to work with.

Each blog post must have an article element, which must also have title, date, and tags children - all of their purpose should already be clear. The article element may also have a comments propery, which, when set, will enable commenting on the particular post.

A blog post must also have a summary element, the contents of which will be used when rendering the post for the purposes of recent posts, or as the summary on the main index page. The summary is also part of the blog post, and when viewing the entire post, it will start with the summary.

Following that, the entire contents of the section element of the blog post will be displayed.

The structure this function generates, should be pretty clear by glancing over the code here.

(defn read-post
  [file]
  (let [post (io/read-file file)
        date (time-format/parse blog-date-format (apply h/text (h/select post [:article :date])))]
    {:title (apply h/text (h/select post [:article :title])),
     :tags (map h/text (h/select post [:article :tags :tag])),
     :summary (h/select post [:summary :> h/any-node]),
     :date date,
     :url (post-url date (.getName file))
     :comments (or
                (-> (first (h/select post [:article])) :attrs :comments utils/enabled?)
                (-> (h/text (first (h/select post [:article :comments]))) utils/enabled?)),
     :content (h/select post [:section :> h/any-node])}))

Blog post templates

Blog posts are almost entirely contained within the #madness-article element, with only the previous/next links outside of it.

One of the first things one sees about a post, is its title, which is the h2 element of the #madness-article in the template.

This snippet uses that element as the title template, replacing the title attribute of it, and its textual content with the title of the post itself.

(h/defsnippet blog-post-title (cfg/template) [:#madness-article :h2]
  [title]
  [:h2] (h/do->
         (h/content title)
         (h/set-attr :title title)))

Full article footer

Posts, when viewed in full, and not only their summary, have a footer, which holds their tags and the date they were posted on, these all live under the #madness-article-meta element.

The full article footer has only a single link in the template: the snippet that we'll use to render a single tag.

(h/defsnippet blog-post-tag (cfg/template) [:#madness-article-tags :a]
  [tag]
  [:a] (h/set-attr :href (utils/tag-to-url tag))
  [:a :span] (h/substitute tag))

The post meta-data - as mentioned just before - lives in #madness-article-meta, and contains the date of the post, and the tags.

(h/defsnippet blog-post-meta (cfg/template)
  [:.madness-article-meta]
  [post]
  [:#madness-article-date] (h/do->
                            (h/set-attr :href (utils/date-to-url (:date post)))
                            (h/content (utils/date-format (:date post))))
  [:#madness-article-tags :a] (h/clone-for
                               [tag (butlast (:tags post))]
                               (h/do->
                                (h/substitute (blog-post-tag tag))
                                (h/after ", ")))
  [:#madness-article-tags] (h/append
                            (blog-post-tag (last (:tags post))))
  [:#madness-article-tags] (h/remove-attr :id))

Post navigation

To ease navigating between posts, previous and next posts (if available) will be shown outside of the #madness-article. These we'll call #madness-article-neighbours, and this element must have two children: #madness-article-next and #madness-article-prev for links to the next and previous posts, respectively.

Both of these need to have an a element, whose href will be rewritten, and that element must have a span child, to be replaced by the title of the previous or next post.

(h/defsnippet blog-post-neighbours (cfg/template) [:#madness-article-neighbours]
  [neighbours]
  [:#madness-article-next :a] (h/set-attr :href (:url (first neighbours)))
  [:#madness-article-next :a :span] (h/substitute (:title (first neighbours)))
  [:#madness-article-next] (if (empty? (first neighbours))
                             nil
                             (h/remove-attr :id))
  [:#madness-article-prev :a :span] (h/substitute (:title (last neighbours)))
  [:#madness-article-prev :a] (h/set-attr :href (:url (last neighbours)))
  [:#madness-article-prev] (if (empty? (last neighbours))
                             nil
                             (h/remove-attr :id))
  [:#madness-article-neighbours] (h/remove-attr :id))

Commenting

If commenting is enabled for a post, the #madness-article-comments element should be left intact, as-is. Otherwise, it will be removed, that is all this snippet does.

(h/defsnippet blog-post-comments (cfg/template) [:#madness-article-comments]
  [post]
  [:#madness-article-comments] (when (:comments post) (h/remove-attr :id)))

A blog post title is contained within #madness-article, in a h2 element, which must have an a child. These two will be updated to contain the title of the given blog post.

(h/defsnippet blog-post-title (cfg/template)
  [:#madness-article :h2]
  [post]
  [:h2] (h/set-attr :title (:title post))
  [:h2 :a] (utils/rewrite-link (:url post) (:title post))
  [:#madness-article] (h/remove-attr :id))

Putting it all together

To put a full blog post together, we alter the page title, disable the recent and archived post areas, rearrange the #madness-article, pull in the next/prev links into #post-neighbours, and last but not least, fill out the global tag & recent post lists, using the tools provided by blog.nav.

(h/deftemplate blog-post (cfg/template)
  [post all-posts]
  [:title] (h/content (:title post) " - Asylum")
  ; Navigation bar
  [:#madness-recent-posts :li] (blog-nav/recent-posts all-posts)
  [:#madness-recent-posts] (h/remove-attr :id)
  [:#madness-tags :li] (blog-nav/all-tags all-posts)
  [:#madness-tags] (h/remove-attr :id)
  ; Article
  [:#madness-article :h2] (h/substitute
                           (blog-post-title post))
  [:#madness-article-content] (h/substitute
                               (:summary post)
                               (:content post))
  [:.madness-article-meta] (h/substitute
                            (blog-post-meta post))
  [:#madness-article-read-more] nil
  ; Footer
  [:#madness-article-comments] (h/substitute (blog-post-comments post))
  [:#madness-article-neighbours] (h/substitute (blog-post-neighbours (utils/neighbours all-posts post)))
  ; Archive
  [:#madness-archive-recent-posts] nil
  [:#madness-archive-archived-posts] nil
  ; Misc
  [:.pygmentize] utils/pygmentize-node
  ; Cleanup
  [:#main-feed] (h/remove-attr :id)
  [:#rss-feed] (h/remove-attr :id)
  [:#madness-content-area] (h/remove-attr :id)
  [:#madness-article] (h/remove-attr :id))

To help cross posting to other engines, lets have a template that only contains the rendered summary and content of the post, and nothing else.

This uses an empty template, but still does code highlighting.

(h/deftemplate blog-post-fragment (cfg/template :empty)
  [post _]
  [:html] (h/do->
           (h/substitute (:summary post)
                         (:content post)))
  [:.pygmentize] utils/pygmentize-node)
 

Loading & rendering of individual static pages

For the sake of ease, static pages are fully loaded first, and turned into a structure that is easy to work with. This namespace implements the low-level loading, restructuring and rendering of individual static pages.

Static pages are under resources/pages by default, and their path below that will be reused as-is for the URL of the generated page. That is, resources/pages/foo/bar/index.html becomes /foo/bar/index.html.

(ns madness.blog.page
  ^{:author "Gergely Nagy <algernon@madhouse-project.org>"
    :copyright "Copyright (C) 2012-2013 Gergely Nagy <algernon@madhouse-project.org>"
    :license {:name "Creative Commons Attribution-ShareAlike 3.0"
              :url "http://creativecommons.org/licenses/by-sa/3.0/"}}
  (:require [net.cgrand.enlive-html :as h]
            [madness.blog.nav :as blog-nav]
            [madness.utils :as utils]
            [madness.config :as cfg]
            [madness.io :as io]
            [clojure.string :as str]
            [clj-time.format :as time-format]))

Given a file path, strips the pages directory, and returns the result, which will be used as the URL for a given page.

(defn- page-url
  [path]
  (second (first (re-seq (re-pattern (str ".*" (cfg/dirs :pages) "(.*)"))
                         path))))

Read a static page from a while, and restructure it into a representation that is easy to work with.

Each static page must have an article element, where the only required child is the title. It can also - optionally - have a comments property, which, when set, will enable commenting on the particular page.

Apart from the article element, only a section element is required, whose contents will be used as the main content of the static page.

The structure this function generates, should be pretty clear by glancing over the code here.

(defn read-page
  [file]
  (let [page (io/read-file file)]
    {:title (apply h/text (h/select page [:article :title])),
     :url (page-url (.getPath file))
     :comments (or
                (-> (first (h/select page [:article])) :attrs :comments utils/enabled?)
                (-> (h/text (first (h/select page [:article :comments]))) utils/enabled?)),
     :content (h/select page [:section])}))

Static page templates

The first thing about a page, is its header, the h2 element of the #madness-article in the template.

This snippet uses that element as the title template, replacing the title attribute of it, and its textual content with the title of the page itself.

(h/defsnippet blog-page-title (cfg/template) [:#madness-article :h2]
  [title]
  [:h2] (h/do->
         (h/content title)
         (h/set-attr :title title))
  [:#madness-article] (h/remove-attr :id))

If commenting is enabled for a post, the #madness-article-comment element should be left intact, as-is. Otherwise, it will be removed, that is all this snippet does.

(h/defsnippet blog-page-comments (cfg/template) [:#madness-article-comments]
  [page]
  [:#madness-article-comments] (when (:comments page) (h/remove-attr :id)))

Putting it all together

To put a full page together, we alter the page title, disable the recent and archived post areas, along with the #madness-article-neighbours, as pages do not have those. We also rearrange the #madness-article, and last but not least, fill out the global tag & recent post list, using the tools provided by blog.nav.

(h/deftemplate blog-page (cfg/template)
  [page all-posts]
  [:title] (h/content (:title page) " - Asylum")
  ; Article
  [:#madness-article :h2] (h/substitute (blog-page-title (:title page)))
  [:#madness-article-content] (h/substitute
                               (:content page))
  [:.madness-article-meta] nil
  [:#madness-article-read-more] nil
  ; Footer
  [:#madness-article-comments] (h/substitute (blog-page-comments page))
  [:#madness-article-neighbours] nil
  [:#madness-archive-recent-posts] nil
  [:#madness-archive-archived-posts] nil
  ; Misc
  [:.pygmentize] utils/pygmentize-node
  ; Navigation bar
  [:#madness-recent-posts :li] (blog-nav/recent-posts all-posts)
  [:#madness-recent-posts] (h/remove-attr :id)
  [:#madness-tags :li] (blog-nav/all-tags all-posts)
  [:#madness-tags] (h/remove-attr :id)
  [:#madness-content-area] (h/remove-attr :id)
  [:#madness-article] (h/remove-attr :id)
  [:#main-rss] (h/remove-attr :id)
  [:#rss-feed] (h/remove-attr :id))
 

Low-level archive rendering

Archives are pages that do not have any content, but the title, an archive-specific Atom feed, and a list of recent posts, and optionally archived posts.

(ns madness.blog.archive
  ^{:author "Gergely Nagy <algernon@madhouse-project.org>"
    :copyright "Copyright (C) 2012-2013 Gergely Nagy <algernon@madhouse-project.org>"
    :license {:name "Creative Commons Attribution-ShareAlike 3.0"
              :url "http://creativecommons.org/licenses/by-sa/3.0/"}}
  (:require [net.cgrand.enlive-html :as h]
            [madness.blog.post :as blog-post]
            [madness.blog :as blog]
            [madness.blog.nav :as blog-nav]
            [madness.utils :as utils]
            [madness.config :as cfg]
            [madness.blog.recent :as blog-recent]
            [clojure.string :as str]))

This snippet renders a single item for the archived posts table. It takes the post as argument, and rewrites the #madness-archive-recent-post item from the template, updating its link and title, and also removing any other content part that appears on a recent post.

(h/defsnippet archive-post-item (cfg/template) [:#madness-archive-recent-post]
  [post]
  [:#madness-archive-recent-post] (h/remove-attr :id)
  [:h3 :a] (utils/rewrite-link-with-title
             (:url post) (:title post))
  [:#madness-recent-article-meta] nil
  [:#madness-recent-article-summary] nil)

Renders a single row of archived posts, using the #madness-archive-recent-post-row element of the template as source.

(h/defsnippet archive-post-row (cfg/template) [:#madness-archive-recent-post-row]
  [posts]
  [:#madness-archive-recent-post-row] (h/remove-attr :id)
  [:#madness-archive-recent-post]
    (h/clone-for [p posts]
                 (h/do->
                  (h/substitute (archive-post-item p))
                  (h/set-attr :class (str "span" (cfg/archive-posts :span)))
                  (h/remove-attr :id)))
  [:#recent-posts] (h/do->
                    (h/remove-attr :id)
                    (h/remove-class "visible-desktop")))

Renders the whole archive page, be that the main one, or the per-tag archives. The page will include the title, a list of recent posts, followed by archived ones, and of course any other wrapping the template may hold. This also updates the Atom feed in the page (the #main-rss and #rss-feed elements) with the supplied URL. Everything else is disabled.

Uses the #madness-archive-recent-posts and #madness-archive-archived-posts elements of the template mostly.

(h/deftemplate blog-archive (cfg/template)
  [title feed-url blog-posts all-posts]
  [:#madness-article :h2] (h/do->
                           (h/content title)
                           (h/set-attr :title title))
  [:.madness-article-meta] nil
  [:#madness-article-content] nil
  [:#madness-article-read-more] nil
  [:#madness-article-comments] nil
  [:#madness-article-neighbours] nil
  [:#rss-feed] (h/do->
                (h/set-attr :href feed-url)
                (h/remove-attr :id))
  [:#main-rss] (h/do->
                (h/remove-attr :id)
                (h/set-attr :href feed-url))
  ; Navigation bar
  [:#madness-recent-posts :li] (blog-nav/recent-posts all-posts)
  [:#madness-recent-posts] (h/remove-attr :id)
  [:#madness-tags :li] (blog-nav/all-tags all-posts)
  [:#madness-tags] (h/remove-attr :id)
  ; Recents & archived posts
  [:#madness-archive-recent-posts] (h/do->
                                    (h/remove-attr :id)
                                    (h/remove-class "visible-desktop"))
  [:#madness-archive-recent-post-row]
    (h/clone-for [rows (utils/blog->table
                        (cfg/recent-posts :columns)
                        (cfg/recent-posts :rows) blog-posts)]
                 (h/do->
                  (h/substitute (blog-recent/recent-post-row rows true))
                  (h/before utils/hr-desktop)))
  [:#madness-archive-archived-posts] (h/remove-attr :id)
  [:#madness-archive-archived-post-row]
     (h/clone-for [rows (utils/blog->table
                         (cfg/archive-posts :columns)
                         (cfg/archive-posts :rows)
                         (drop (* (cfg/recent-posts :columns)
                                  (cfg/recent-posts :rows)) blog-posts))]
                  (h/do->
                   (h/substitute (archive-post-row rows))))
  ; Cleanup
  [:#madness-content-area] (h/remove-attr :id)
  [:#madness-article] (h/remove-attr :id))
 

Rendering the index page

The index is the root document of the generated site, it has a featured - the most recent - blog post, and a list of recent items, but no archived ones.

(ns madness.blog.index
  ^{:author "Gergely Nagy <algernon@madhouse-project.org>"
    :copyright "Copyright (C) 2012-2013 Gergely Nagy <algernon@madhouse-project.org>"
    :license {:name "Creative Commons Attribution-ShareAlike 3.0"
              :url "http://creativecommons.org/licenses/by-sa/3.0/"}}
  (:require [net.cgrand.enlive-html :as h]
            [madness.blog :as blog]
            [madness.blog.nav :as blog-nav]
            [madness.utils :as utils]
            [madness.config :as cfg]
            [madness.blog.recent :as blog-recent]
            [madness.blog.post :as blog-post]
            [clojure.string :as str]))

The featured article has a "Read on" button, which is the #madness-article-read-more element. It should be, or should have an a child, whose href this snippet sill replace, and remove the id.

(h/defsnippet index-read-on (cfg/template) [:#madness-article-read-more]
  [post]
  [:#madness-article-read-more :a] (h/set-attr :href (:url post))
  [:#madness-article-read-more] (h/remove-attr :id))

Putting it all together

The index page has a featured article, in the #madness-article element, a set of recent posts in the #madness-recent-posts element, generated using the tools provided by blog.recent.

There are no neighbours, no archive, no comments, but the global tag and recent post lists are, of course, filled out, using the functions provided by blog.nav.

(h/deftemplate blog-index (cfg/template)
  [blog-posts _]
  [:#madness-article :h2] (h/substitute (blog-post/blog-post-title (first blog-posts)))
  [:#madness-article-content] (h/substitute (:summary (first blog-posts)))
  [:.madness-article-meta] (h/substitute
                            (blog-post/blog-post-meta (first blog-posts)))
  [:#madness-article-read-more :a] (h/set-attr :href (:url (first blog-posts)))
  [:#madness-article-read-more] (h/remove-attr :id)
  [:#madness-article-comments] nil
  [:#madness-article-neighbours] nil
  ; Navigation bar
  [:#madness-recent-posts :li] (blog-nav/recent-posts blog-posts)
  [:#madness-recent-posts] (h/remove-attr :id)
  [:#madness-tags :li] (blog-nav/all-tags blog-posts)
  [:#madness-tags] (h/remove-attr :id)
  ; Index
  [:#madness-archive-recent-posts] (h/remove-attr :id)
  [:#madness-archive-recent-post-row]
    (h/clone-for [rows (utils/blog->table
                        (cfg/recent-posts :columns)
                        (cfg/recent-posts :rows) (rest blog-posts))]
                 (h/do->
                  (h/substitute (blog-recent/recent-post-row rows false))
                  (h/before utils/hr-desktop)))
  [:#madness-archive-archived-posts] nil
  ; Cleanup
  [:#main-rss] (h/remove-attr :id)
  [:#rss-feed] (h/remove-attr :id)
  [:#madness-content-area] (h/remove-attr :id)
  [:#madness-article] (h/remove-attr :id)
  [:.no-index] nil)
 

Rendering Atom feeds

Atom feeds are considerably easier to render than HTML pages, there is much less markup and design to pay attention to.

(ns madness.blog.atom
  ^{:author "Gergely Nagy <algernon@madhouse-project.org>"
    :copyright "Copyright (C) 2012 Gergely Nagy <algernon@madhouse-project.org>"
    :license {:name "Creative Commons Attribution-ShareAlike 3.0"
              :url "http://creativecommons.org/licenses/by-sa/3.0/"}}
  (:require [clj-time.format :as time-format]
            [clj-time.local :as time-local]
            [net.cgrand.enlive-html :as h]
            [clojure.string :as str]
            [madness.config :as cfg]))

Atom feeds need a special date format, this is that one.

(def atom-date-formatter
  (time-format/formatter "yyyy-MM-dd'T'HH:mm:ssZZ"))

We'll need to include the full post in the Atom feed, the summary and the content after one another, but we need to render only those, and those alone, without the rest of the design.

For this, we use an empty template, and replace it wholly with the rendered summary + content combination.

(h/deftemplate bare-post (cfg/template :empty)
  [post]
  [:html] (h/substitute (:summary post) (:content post)))

Since Atom feeds are published, links therein should be absolute. This little function replaces all local links - any, that starts with / - with their expanded, fully qualified version.

(defn local-href-expand
  [feed]
  (str/replace feed #"href=\"(/[^\"]*)\""
               (str "href=\"" (cfg/atom-feed :base-url) "$1\"")))

Atom entries have categories, this snippet takes the category element of an entry from the template, and fills it out, according to the tag given as a parameter.

(h/defsnippet atom-post-tag (cfg/template :atom) [:entry :category]
  [tag]
  [:category] (h/do->
               (h/set-attr :term (str/replace (str/lower-case tag) " " "-"))
               (h/set-attr :label tag)))

A single atom post has a title, a link, an updated and published date, an id, content and multiple category elements. This snippet takes the entry element from the source template, and fills it in appropriately.

(h/defsnippet atom-post (cfg/template :atom) [:entry]
  [site-base post]
  [:title] (h/content (:title post))
  [:link] (h/set-attr :href (str site-base (:url post)))
  [:updated] (h/content (time-format/unparse
                         atom-date-formatter (time-local/to-local-date-time (:date post))))
  [:published] (h/content (time-format/unparse
                           atom-date-formatter (time-local/to-local-date-time (:date post))))
  [:id] (h/content (str site-base (:url post)))
  [:category] (h/clone-for
               [tag (:tags post)]
               (h/substitute (atom-post-tag tag)))
  [:content] (h/content (apply str (bare-post post))))

Finally, assembling the whole atom feed is as simple as setting a the title and id under feed, with the site title and our base uri, respectively, updating the updated element, and adding the entries, by cloning the atom-post snippet above for each entry within the feed.

The Atom feed also has two links: one with rel=self, and another, the base URI. The former must be marked with a #self id in the source template, the latter with #base. The extra ids will be removed from the result, they're only there so that the parts can be easily identified within the template.

(h/deftemplate atom-feed (cfg/template :atom)
  [title uri site-base posts]
  [:feed :title] (h/content title)
  [:feed] (h/set-attr :xmlns "http://www.w3.org/2005/Atom")
  [:#self] (h/do->
            (h/remove-attr :id)
            (h/set-attr :href (str site-base uri "atom.xml")))
  [:#base] (h/do->
            (h/remove-attr :id)
            (h/set-attr :href (str site-base uri)))
  [:feed :id]
    (h/content (str site-base uri))
  [:updated] (h/content (time-format/unparse atom-date-formatter (time-local/local-now)))
  [:entry] (h/clone-for [p posts]
                        (h/substitute (atom-post site-base p))))

To finalise the Atom feed, local links shall be expanded, after the feed has been generated. This function puts that all together, and should be the only function used from this namespace.

(defn emit-atom
  [title uri posts]
  (local-href-expand (apply str (atom-feed title uri
                                           (cfg/atom-feed :base-url) posts))))