correct • elegant • free

Site now in Yesod

I lost the Drupal version of this site during a VPS move. Careless, I know, but also an indication of one of the troubles with Content Management Systems: your data is in a database under a schema of doubtful provenance.

Actually, all my words were in reST files (there's an article here about how I injected them into Drupal in the first place), so there wasn't too much actually lost. I still have all the content, and at least the most important levels of structure: "all these pages belong together in a book" is represented by "all these reST files live in the same directory". What I have lost is meta-data like tags, and comments (I only ever had 2 comments that weren't spam, which is a subject for another entry).

It's interesting to speculate how much work it would have been to get the Drupal site up and running again. Supposing I'd remembered to dump the database before switching off my last VPS. I could, no doubt, have restored that into a modestly different version of PostgreSQL without too many difficulties. And I expect Drupal is quite happy to migrate from old database schemas.

But still, it's a worry even after a couple of years. And presumably one day Drupal, and perhaps even PostgreSQL will stop working altogether. Which all makes me suspect that if you've got content that you really care about, dropping it into a Content Management System may not be the very best thing you can do with it. I'm not sure what is. But a bunch of files, that are comprehensible with nothing more than cat, should still make sense 100 years from now.

Anyway, I'd decided to learn Yesod as I suspect it will be the platform of choice for the upcoming rewrite of my ecommerce site. And, like so many things Haskell, it takes a little while to wrap your head round it, but once you have, it's jus-telegant!

Still a few things missing: Atom / RSS feeds, comments (maybe), and the styling is still not quite to my taste. But overall, I'm deeply impressed with Yesod.

You might be wondering what I'm talking about. Drupal is a Content Management System, and Yesod is a Web Application Framework. Apples and oranges? Well, yes. You could Drupal (or WordPress) the rest of the week, without seeing a single HTML tag, CSS property, or line of PHP code. But as it happens I know HTML, and CSS (I don't claim to know much of PHP, nor do I want to). So the Drupal phase is perhaps best seen as me learning what the CMS way is; it was probably never going to be a long-term home for the site.

What all this means, is that effectively I'm implementing a custom CMS. Won't that be a lot of work? Well, fortunately my needs are fairly modest, and Yesod is very powerful. For example, consider the "book" concept. As already mentioned, that's represented in the raw content as a bunch of files in a directory. To turn them into a book with Yesod, first I tell it how to handle a URL like /book/doio by adding this line to config/routes:

/book/#String BookR GET

Next, I define the corresponding handler. It will be passed a single argument, in this case doio:

getBookR :: String -> Handler RepHtml
getBookR book =
  let dir = docDir ++ book ++ "/"
      urp = "/book/" ++ book
  in do
    files <- liftIO $ getBookFiles dir
    let paths0 = map (takeWhile (/= '.')) $ tail files
        paths1 = map (("/chapter/" ++ book ++ "/") ++) paths0
    chapTitles <- liftIO $ mapM getTitleForPath paths1
    preString <- liftIO $ readFile $ dir ++ (head files)
    thisTitle <- liftIO $ getTitleForPath urp
    let preface = HTML.preEscapedString preString
        chapters = zip paths1 chapTitles
    myLayout urp $ do
      setTitle $ HTML.preEscapedString $ thisTitle ++ titleSuffix
      addWidget $(widgetFile "book")

OK, so there's quite a lot going on there. But it boils down to this. First read the directory; the first file is the "preface" that should appear on the book page, its contents goes into the variable preface. Each remaining file is a "chapter" of the book, so construct a list of URLs for each chapter and a list of titles for each chapter; then zip those two together as chapters. Finally call the overall layout function, passing it the original URL, and a widget built by combining a title with the "book" widget.

The book widget looks like this:

  $forall chapter <- chapters
      <a href="#{fst chapter}">#{snd chapter}

It's written in "Hamlet", which is a minimalist take on HTML, using indents to indicate nesting. We start with the preface (all names that are in scope when the $(widgetFile "book") splice is interpolated are available to Hamlet). Then loop over the chapters list, and build a link to each one. Here's the end result.