Draft Posts with Hakyll

Posted on September 24, 2014
Tags: hakyll

Draft Posts are posts that don’t show up in your posts lists, but are available if you know the URL, so you can share them easily with a few selected editors before you officially publish them. They should not appear in the site’s archive or in any feed until published.

There are many different approaches to add support for draft posts to Hakyll. Most of them involve a special metadata field, for example “published: true” or “draft: true”, to mark such posts as drafts. I decided against a metadata field, in favor of a simpler solution: Draft posts are put into the posts/drafts folder. In order to publish them, you simply move them into the posts/ folder.

First we need a filter function that will filter out all draft posts from appearing in the archive or the feeds.

nonDrafts :: (MonadMetadata m, Functor m) => [Item a] -> m [Item a]
nonDrafts = return . filter f
  where
    f = not . isPrefixOf "posts/drafts/" . show . itemIdentifier

Then we create a new function that will replace the existing recentFirst function.

recentFirstNonDrafts :: (MonadMetadata m, Functor m) => [Item a] -> m [Item a]
recentFirstNonDrafts items = do
                       nondrafts <- nonDrafts items
                       recentFirst nondrafts

Now s/recentFirst/recentFirstNonDrafts in you hakyll configuration (usually site.hs). And since we introduced a sub folder in posts/, the glob for posts need to be changed to look into sub folders too. This is done by s;posts/*;posts/**;

Because draft posts don’t have a date set, we need to set a default date for every post without a date. First you need to configure a context that will add a default date for every draft post

defaultDateContext :: Context a
defaultDateContext = Context $ \k i ->
  let itemPath = show $ itemIdentifier i in
  if (isPrefixOf "posts/drafts/" itemPath) && (k == "date")
    then (\_ -> do return (StringField "1970-01-01")) i
    else empty

Then this context needs to configured as postCtx. I do this with

postCtx :: Tags -> Context String
postCtx tags = mconcat
    [ defaultContext
    , defaultDateContext
    , dateField "date" "%B %e, %Y"
    , tagsField "tags" tags
    ]

Finally I’ve written a little convenience script to publish the posts. It will check if the git repository is clean, move the draft posts into the posts/ folder and prepend a date string (i.e. the date the post got published).

#!/bin/bash -e

if ! [[ -f $1 ]]; then
	echo "$1 is not a file"
	exit 1
fi

if ! git diff --exit-code; then
	echo "Error: Unstaged changes found, please stage your changes"
	exit 1
fi

if ! git diff --cached --exit-code; then
	echo "Error: Staged, but uncommited changes found, please commit"
	exit 1
fi

declare -r POST=$(date +%Y-%m-%d)-$(basename ${1})

git mv $1 posts/${POST}

git commit -m "Published $POST"