Automated Beautiful Blog Post Images Using Hakyll

January 25, 2016

I love the idea of beautiful large header images in blog posts, but I absolutley hate having to scour the internet searching for images that will match the theme of what I’m talking about. So as any good programmer would, I decided to automated this process so I never have to think about picking an image again.

The image you see above this text was generated by an awesome library written by Tom Smeets called FractalArt (technically not fractals but this is besides the point). As soon as I read this reddit post, I knew I wanted to use the library immediatley.

What I Did

I wrote a function that runs before hakyll begins compiling the static site. If a new blog post is detected, this function writes the name of the image file it will create to the markdown of the blog post. For example, this is what the header of this blog post looks like in markdown:

---
title: Automated Beautiful Blog Post Images Using Hakyll
---

After the function runs, it takes the name of this markdown file (2016-01-25-blogPostImages.md) and does this:

---
title: Automated Beautiful Blog Post Images Using Hakyll
headerImg: 2016-01-25-blogPostImages.png
---

My function then runs FractalArt and saves the image under the same name in my images directory. Hakyll then handles linking the title of this image to the actual file in the post.html template.

Running It

I’ve written my code in site.hs so that before I compile the static site, it firsts creates all the images for any new blog posts. This is what the output looks like for this blog post that I’m currently writing:

$ stack exec blog build

Wrote header images to the follows blog posts:
"2016-01-25-blogPostImages"

Generated the following images:
"2016-01-25-blogPostImages.png"

<hakyll compilation omitted>

It detected that this blog post didn’t contain an image so it generated one.

How I did It

The first thing I did was to take take Tom Smeet’s FractalArt program and completely gut the whole thing to remove anything I didn’t need. This included all the foreign functions he called to access screen size. I then refactored the main file and created the following function which takes in a filepath and generates an image at that file path:

generateArt :: FilePath -> IO ()
generateArt imageFile = do ...

A gist of the full version of the FractalArt file that I cleaned up can be found here.

Th main function in site.hs is shown below. It generates images for any new blog posts and then compiles the site.

main :: IO ()
main = do
    generateBlogPostImages
    compileStaticSite

The function “generateBlogPostImages” is defined as follows:

generateBlogPostImages :: IO ()
generateBlogPostImages = do
    -- Fractal Art blog image generation
    pwd <- getCurrentDirectory

    let imageDirectory = pwd </> "static" </> "img/"
        postsDirectory = pwd </> "posts/"

    posts <- listDirectory postsDirectory
    allImages <- listDirectory imageDirectory
    let cleanedPosts = removeDSStore posts
        cleanedImages = removeDSStore allImages
        imgNames = imageNames cleanedPosts
        imgsToGen = imageNamesToGenerate cleanedImages imgNames

    -- Specify image names in each blog post markdown
    filesWrittenTo <- sequence $ (writeHeaderImg postsDirectory) <$> cleanedPosts

    -- Print which files were written to
    let writF = writtenFiles filesWrittenTo
    if length writF > 0 then do
        putStrLn "Wrote header images to the follows blog posts:"
        mapM_ print writF
        putStrLn ""
    else return ()

    -- Generate image if it doesn't exist
    let imgsToGenFps = (imageDirectory++) <$> imgsToGen
    sequence_ $ generateArt <$> imgsToGenFps

    -- Print which images were generated
    if length imgsToGenFps > 0 then do
        putStrLn "Generated the following images:"
        mapM_ print imgsToGen
        putStrLn ""
    else return ()

This function first specifies the images and posts directories. It then determines all the names of the image files it needs to generate. It uses this list to check the images directory for which images need to actually be generated. This is done incase we delete a blog post image. The result of this is imgsToGen which is the list containing the names of the images that still need to be generated. We then map writeHeaderImg over all the posts in our posts directory. The function writeHeaderImg is shown below:

writeHeaderImg :: FilePath -> FilePath -> IO (Maybe FilePath)
writeHeaderImg wd fp = do
    let filePath = wd </> fp
    postContents <- readFile filePath
    let fileName = getFileName fp
        postContentsList = lines postContents
        thirdLine = postContentsList !! 2
    if (thirdLine == "---") then do
        let headerImgLine = mconcat ["headerImg: ", fileName, ".png"]
            newContents = insertAt 2 headerImgLine postContentsList
            tempFile = wd </> "temp.markdown"
        _ <- writeFile tempFile $ unlines newContents
        _ <- copyFile tempFile filePath
        removeFile tempFile
        return $ Just fileName
    else return Nothing
        where insertAt :: Int -> String -> [String] -> [String]
              insertAt n s xs = fs ++ [s] ++ ls
                  where fs = take n xs
                        ls = drop n xs

This function opens the file, and checks if it contains a headerImg title. If it doesn’t it writes it, otherwise it skips it. Back in the generateBlogPostImages function, the next thing we do is take the list of the names of images we need to generate and convert them to filepaths. Finally, we map the function we created in FractalArt (generateArt :: FilePath -> IO ()) over this list of filepaths.

And that’s it!

Everytime I write a new blog post, all I have to do is deploy my site and the function I wrote does everything else for me. Long gone are the days of having to waste time finding a suitable image. If you want to see the full code to get a clearer sense of what I did, you can check out the source code of this blog here.