I'm Different
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.