Blogging with chicken scheme -- Hyde compiles for the web

Introduction

As you might have noticed from the changed design of the blog something has changed. And indeed it has! What the honorable reader is now looking at came straight out of the nice website generator Hyde. Similar to its other cousins Webby and Jekyll it generates websites. Unlike its cousins it is written in CHICKEN scheme my favorite programming language.

Hyde will generate XHTML, XML, atom feeds or any other output for which you provide a recipe for.

It is in use by the Chicken Gazette - Chicken Scheme's weekly newsletter.

Markup

Hyde accepts SXML and svnwiki by default, others can be build in. I decided to stick to svnwiki's (or qwiki's) for convenience. A typical blogpost looks like this:

((title . "Blogging with chicken scheme -- Hyde compiles for the web")
 (tags chicken scheme blog)
 (layouts "blogpost.sxml" "default.sxml")
 (date . 1284929501.0))

=== Introduction

As you might have noticed from the changed design of the blog
something has changed. And indeed it has! What the honorable reader is
...

As you can see, each post starts with an s-expression giving it a title, the post date (in epochs) and a list of tags. After that all text follows. The chicken wiki's edit help has a comprehensive list of the allowed markup tags.

Blogging with hyde

Now that we have this nice website generator we need to add some blogging functionality to it.

I wanted to have

To get all these features I needed to implement the index and archive page. For the shortest path to success I decided to have a tags page that lists each post under every tag. Links from the tag cloud would then point to the specific anchor on the tags page.

So my index page looks now like this:

((title . "Recent posts"))
`((div (@ (id "tagsoup"))
     (p "This blog covers: ")
     ,(map (lambda (t)
         `((a (@ (href ,(root-path)"/tags.html#",(car t))
                 (class ,(tag-class t)))
             ,(car t))" "))
       (count-tags (all-pages))))
  (h1 ,($ 'title))
  ,(map (lambda (page)
       `((div (@ (class "article-meta"))
              (a (@ (class "article-title")
                    (href ,(page-url page)))
		,($ 'title page))
                (div (@ (class "date"))
                     "posted on "
                     ,(neat-date (page-updated page)))
                (inject ,(read-page page)))))
     (if ( > (length (all-pages)) (max-articles))
         (take (all-pages) (max-articles))
         (all-pages)))
  ,(when (> (length (all-pages)) (max-articles))
      '(a (@ (href "archive.html"))
         "Older articles")))

First the title of the index page is set, then the div element for the tag cloud is filled. Following the tags (max-articles) posts will be rendered, enclosed in its own div with title and posting information.

How does this transform into HTML? This is where hyde's layouts come into play. You can either specify a list of layouts in each page's header using the layouts clause or specify the global default layout.

This is the default layout for this blog:

((main-title "/dev/random - Random Thoughts On Programming With Parenthesis"))
`((xhtml-1.0-strict)
  (html (@ (xmlns "http://www.w3.org/1999/xhtml"))
        (head
	 (meta  (@ (http-equiv "content-type") (content "application/xhtml+xml; charset=UTF-8")))
         (link (@ (href "http://fonts.googleapis.com/css?family=Vollkorn:regular,italic,bold,bolditalic") (rel "stylesheet") (type "text/css")))
         (link (@ (href  "http://fonts.googleapis.com/css?family=Inconsolata") (rel "stylesheet") (type "text/css")))
	 (link (@ (href ,(root-path)"devrandom.css") (rel "stylesheet") (type "text/css")))
	 (link (@ (rel "alternate") (href ,(root-path)"feed.atom") (type "application/atom+xml")))
	 (title ,($ 'main-title) ,(and ($ 'title) (conc " - " ($ 'title)))))
        (body
         (div (@ (class "navigation"))
              ,(navigation-links)
              ,($ 'main-title))
         (div (@ (id "content"))
              (inject ,contents))
         (div (@ (class "fineprint"))
                 "Unless otherwise credited all material © 2010 Christian Kellermann"))))

And each blogpost gets wrapped in a blogpost layout:

()
`((div (@ (class "article-meta"))
       (h1 (@ (class "title")) ,($ 'title))
       (div (@ (class "tags"))
	  "Tagged as: "
	  ,(fold (lambda (t tags)
		   (append tags
			   `(,(and (pair? tags) ", ")
			     (span (@ (class "tag")) ,t))))
		 '()
		 (or ($ 'tags) '())))
       (div  (@ (class "date"))
       "written on "  ,(neat-date ($ 'date))))
  (div (@ (class "article-content"))
  (inject ,contents)))

Ok, now we do have an index page. You might have wondered where the non standard procedures like (navigation-links), (tag-class) and (count-tags) have been defined. All of them are part of the site's hyde script which is just another scheme program.

A lot of inspiration and code has been taken from the Chicken Gazette's repository.


(define (page-url #!optional (page (current-page)))
  (string-append (root-path) (page-path page)))

(define (neat-date d)
  (time->string (seconds->utc-time d) "%Y-%m-%d %Z"))

(define (count-tags pages)
  (let ((tags '()))
    (map (lambda (page)
           (map (lambda(t) (let ((new-v (or (alist-ref t tags equal?) '())))
                             (set! tags (alist-update! t (cons (list ($ 'title page) (page-url page)) new-v) tags equal?))))
                (or ($ 'tags page) '())))
         pages)
    tags))

(define (tag-class t)
  (let* ((ranges '((2 "low")
                  (5 "medium")
                  (6 "high")))
        (class (filter (lambda (a) (<= (length (cdr t)) (car a))) ranges)))
    (string-append "tag-" (if (pair? class)
                              (cadar class)
                              (cadar ranges)))))

(define (navigation-links)
  (let ((nav '(("Home" "index.html")
              ("Tags" "tags.html")
              ("Archive" "archive.html"))))
    (map (lambda (l) `((a (@ (href ,(root-path)"/",(cdr l))) ,(car l)) " | ")) nav)))

(for-each (lambda (binding)
	    (apply environment-extend! (cons (page-eval-env) binding)))
	  `((page-updated ,page-updated)
            (neat-date ,neat-date)
            (count-tags ,count-tags)
            (max-articles ,(lambda () 5))
            (navigation-links ,navigation-links)
            (page-url ,page-url)
            (root-path ,root-path)
            (tag-class ,tag-class)
	    (all-pages ,(lambda ()
			  (sort-by (pages-matching "posts/.+") page-updated)))))

Some other procedures have been omitted. The last for-each loop inserts our own procedures into the page's evaluation environment.

The CSS for the site has been written in SCSS and converted to CSS automatically by hyde.

Atom feeds are generated similar to the index page: (Again this has been taken from the Chicken Gazette's script)

((title . "/dev/random")
 (subtitle . "Random Thoughts On Programming With Parenthesis")
 (date . "2010-08-29")
 (base-uri . "http://pestilenz.org/~ckeen/blog/"))

(let* ((entries (all-pages))
       (seconds->rfc3339-string (lambda (s)
				  (rfc3339->string (seconds->rfc3339 s))))
       (seconds->YYYY-MM-DD (lambda (s)
			      (time->string (seconds->utc-time s) "%Y-%m-%d"))))

  (make-atom-doc
   (make-feed
    title: (make-title ($ 'title))
    subtitle: (make-subtitle ($ 'subtitle))
    updated: (seconds->rfc3339-string
	      (fold (lambda (p c)
		      (let ((p ($ 'date p)))
			(if (and c (> c p)) c p)))
		    #f
		    entries))
    id: (format ($ 'tag) ($ 'date) "/")
    links: (list (make-link uri: (page-url) relation: "self" type: 'atom))
    entries: (map (lambda (p)
		    (make-entry title: (make-title ($ 'title p))
				published: (seconds->rfc3339-string ($ 'date p))
				updated: (seconds->rfc3339-string (page-updated p))
                                authors: (list (make-author name: "Christian Kellermann"))
				id: (format ($ 'tag) (seconds->YYYY-MM-DD ($ 'date p)) (page-path p))
				links: (list (make-link uri: (page-url p) type: 'xhtml))
				content: (make-content (read-page p) type: 'html)))

		  entries))))

I guess that shows most of the functionality. More shall be revealed in following posts.

Code on this site is licensed under a 2 clause BSD license, everything else unless noted otherwise is licensed under a CreativeCommonsAttribution-ShareAlike3.0UnportedLicense