I'm Different

Posted on May 22, 2016 by Tommy M. McGuire
Labels: haskell

I’m different!

– Crow T. Robot

Well, lookie here! I’ve done it. I’ve converted this blog from Blogger to a self-hosted Hakyll setup. And it only took several hundred automated Lego ferrets a couple of days.

The process wasn’t actually too difficult. Blogger supplies a .xml backup, which can be parsed by Jekyll‘s jekyll-import command to produce .html files which can be simply plopped into a Hakyll posts directory. Almost. jekyll-import leaves the tags component of the posts’ metadata blocks in a YAML-style, one-tag-per-line with a hyphen prefix; Hakyll wants them to be comma-separated on one line. Further, setting replace-internal-link to true is needed to transform internal links in the posts into a URL that can be man-handled into something relative.

Here’s an ed script to do the heavy lifting.

/tags:/+1,/^m/-1 s/$/,/
/tags:/+1,/^m/-1 s/-//
/tags:/,/^m/-1 j
s/,$//
1,$ s/{{ site.baseurl }}//g
1,$ s/{% post_url \([^ ]*\) %}/\/posts\/\1.html/g
w
q

The first four lines handle the tag situation, first adding a comma to the end of every tag line, then stripping off the hyphen, and then joining the lines into one, and finally removing the terminal, extraneous comma. The next two lines handle URLS, first by removing the site.baseurl variable and then transforming the post_url thing into a default Hakyll /posts/... local URL.

Note: I’ve since altered how Hakyll generates URLs for posts, so the internal links are all broken. I just realized I need to train a ferret to go through and fix them up again. Geeze. Is my work never done? Do you all realize how much training snackies cost these days?

Copying comments out of the backup required a combination of hacking on jekyll-import and manual work; existing comments should be preserved but won’t be pretty. Sorry.

Anyway, for anyone interested, here’s my current site.hs:

main :: IO ()
main = hakyllWith hakyllConfig $ do
    match "images/*" $ do
        route   idRoute
        compile copyFileCompiler

    match "css/*" $ do
        route   idRoute
        compile compressCssCompiler

These two rules copy the images and css directories into their appropriate locations.

    tags <- buildTags "posts/*" (fromCapture "tags/*.html")

    tagsRules tags $ \tag pattern -> do
      let title = "Posts tagged \"" ++ tag ++ "\""
      route idRoute
      compile $ do
        posts <- chronological =<< loadAll pattern
        let ctx = constField "title" title `mappend`
                  listField "posts" postCtx (return posts) `mappend`
                  defaultContext
        makeItem ""
          >>= loadAndApplyTemplate "templates/tag.html" ctx
          >>= loadAndApplyTemplate "templates/default.html" ctx
          >>= relativizeUrls

This bit is pretty much cobbled together from examples in the documentation, to generate tags pages (click on the haskell link above to see one).

    match "pages/*" $ do
      route $ gsubRoute "pages/" (const "p/") `composeRoutes` setExtension "html"
      let tagsCtx = postCtxWithTags $ sorted tags
      compile $ pandocCompiler
        >>= loadAndApplyTemplate "templates/post.html"    tagsCtx
        >>= loadAndApplyTemplate "templates/default.html" tagsCtx
        >>= relativizeUrls

    match "posts/*" $ do
        route $ customRoute oldStylePath `composeRoutes` setExtension "html"
        let tagsCtx = postCtxWithTags $ sorted tags
        compile $ pandocCompiler
            >>= saveSnapshot "content"
            >>= loadAndApplyTemplate "templates/post.html"    tagsCtx
            >>= loadAndApplyTemplate "templates/default.html" tagsCtx
            >>= relativizeUrls

Next are rules for pages and posts directories; pages are the Web Authentication and Parsing with Derivatives pages in the header. What you are currently reading is a post.

    create ["atom.xml"] $ do
      route idRoute
      compile $ do
        let feedCtx = postCtx `mappend` bodyField "description"
        posts <- fmap (take 10) . recentFirst =<< loadAllSnapshots "posts/*" "content"
        renderAtom feedConfiguration feedCtx posts

    create ["rss.xml"] $ do
      route idRoute
      compile $ do
        let feedCtx = postCtx `mappend` bodyField "description"
        posts <- fmap (take 10) . recentFirst =<< loadAllSnapshots "posts/*" "content"
        renderRss feedConfiguration feedCtx posts

And these two rules create the RSS and Atom feeds, as plain XML files.

    create ["archive.html"] $ do
        route idRoute
        compile $ do
            posts <- recentFirst =<< loadAll "posts/*"
            let archiveCtx =
                    listField "posts" postCtx (return posts) `mappend`
                    constField "title" "Archives"            `mappend`
                    defaultContext

            makeItem ""
                >>= loadAndApplyTemplate "templates/archive.html" archiveCtx
                >>= loadAndApplyTemplate "templates/default.html" archiveCtx
                >>= relativizeUrls


    match "index.html" $ do
        route idRoute
        compile $ do
            posts <- fmap (take 10) . recentFirst =<< loadAll "posts/*"
            let indexCtx =
                    listField "posts" postCtx (return posts) `mappend`
                    constField "title" "Home"                `mappend`
                    tagCloudField "tagCloud" 50 150 tags     `mappend`
                    defaultContext

            getResourceBody
                >>= applyAsTemplate indexCtx
                >>= loadAndApplyTemplate "templates/default.html" indexCtx
                >>= relativizeUrls

    match "templates/*" $ compile templateBodyCompiler

Then it creates the archive and index pages.

The only part that isn’t taken more or less directly from some tutorial or example code is the custom route for pages, which is used to convert what Hakyll would come up with normally into what Blogger used.

-- Blogger paths for posts were /year/month/titlish; this function converts a
-- filename starting with year-month-date-titlish and turns it into a filepath
-- matching the Blogger format.
oldStylePath :: Identifier -> FilePath
oldStylePath ident = year </> month </> titlish
  where basename = takeBaseName $ toFilePath ident
        parts = splitAll "-" basename
        [year,month] = take 2 parts
        titlish = intercalate "-" $ drop 3 parts

The formatting is done with Bootstrap, the fonts Merriweather and Merriweather Sans are from Google Fonts, and I added a Disqus comment thingy. I have also added MathJax for the occasional math formatting (so don’t complain if you momentarily see some TeX which is replaced by pretty mathy stuff).

And then a miracle occurs, and Maniagnosis exsists. Can I get an Amen?

Now, as to why I did it…I’m afraid I don’t understand the question.

active directory ashurbanipal authentication books c c++ comics conference continuations coq data structure digital humanities Dijkstra eclipse virgo electronics emacs goodreads haskell http java job Knuth ldap link linux lisp math naming nimrod notation OpenAM osgi parsing pony programming language protocols python quote R random REST ruby rust SAML scala scheme shell software development system administration theory tip toy problems unix vmware yeti
Member of The Internet Defense League
Site proudly generated by Hakyll.